<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<style>
    html {
        height: 100%;
    }

    body {
        min-height: 100%;
        margin: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #435;
        color: white;
        font-family: sans-serif;
        text-align: center;
    }

    canvas {
        display: block;
        margin: 0 auto;
        cursor: move;
    }
</style>

<body>
    <div class="container">
        <canvas></canvas>
        <p>Click & drag to rotate</p>
    </div>
    <script>
        // -------------------------- utils -------------------------- //

        var TAU = Math.PI * 2;

        function extend(a, b) {
            for (var prop in b) {
                a[prop] = b[prop];
            }
            return a;
        }

        function lerp(a, b, t) {
            return (b - a) * t + a;
        }

        function modulo(num, div) {
            return ((num % div) + div) % div;
        }

        // -------------------------- Vector3 -------------------------- //

        function Vector3(position) {
            this.set(position);
        }

        Vector3.prototype.set = function (pos) {
            pos = Vector3.sanitize(pos);
            this.x = pos.x;
            this.y = pos.y;
            this.z = pos.z;
            return this;
        };

        Vector3.prototype.rotate = function (rotation) {
            if (!rotation) {
                return;
            }
            this.rotateZ(rotation.z);
            this.rotateY(rotation.y);
            this.rotateX(rotation.x);
            return this;
        };

        Vector3.prototype.rotateZ = function (angle) {
            rotateProperty(this, angle, 'x', 'y');
        };

        Vector3.prototype.rotateX = function (angle) {
            rotateProperty(this, angle, 'y', 'z');
        };

        Vector3.prototype.rotateY = function (angle) {
            rotateProperty(this, angle, 'x', 'z');
        };

        function rotateProperty(vec, angle, propA, propB) {
            if (angle % TAU === 0) {
                return;
            }
            var cos = Math.cos(angle);
            var sin = Math.sin(angle);
            var a = vec[propA];
            var b = vec[propB];
            vec[propA] = a * cos - b * sin;
            vec[propB] = b * cos + a * sin;
        }

        Vector3.prototype.add = function (vec) {
            if (!vec) {
                return;
            }
            vec = Vector3.sanitize(vec);
            this.x += vec.x;
            this.y += vec.y;
            this.z += vec.z;
            return this;
        };

        Vector3.prototype.multiply = function (vec) {
            if (!vec) {
                return;
            }
            vec = Vector3.sanitize(vec);
            this.x *= vec.x;
            this.y *= vec.y;
            this.z *= vec.z;
            return this;
        };

        Vector3.prototype.lerp = function (vec, t) {
            this.x = lerp(this.x, vec.x, t);
            this.y = lerp(this.y, vec.y, t);
            this.z = lerp(this.z, vec.z, t);
            return this;
        };

        // ----- utils ----- //

        // add missing properties
        Vector3.sanitize = function (vec) {
            vec = vec || {};
            vec.x = vec.x || 0;
            vec.y = vec.y || 0;
            vec.z = vec.z || 0;
            return vec;
        };

        // -------------------------- PathAction -------------------------- //

        function PathAction(method, points, previousPoint) {
            this.method = method;
            this.points = points.map(mapVectorPoint);
            this.renderPoints = points.map(mapVectorPoint);
            this.previousPoint = previousPoint;
            this.endRenderPoint = this.renderPoints[this.renderPoints.length - 1];
            // arc actions come with previous point & corner point
            // but require bezier control points
            if (method == 'arc') {
                this.controlPoints = [new Vector3(), new Vector3()];
            }
        }

        function mapVectorPoint(point) {
            return new Vector3(point);
        }

        PathAction.prototype.reset = function () {
            // reset renderPoints back to orignal points position
            var points = this.points;
            this.renderPoints.forEach(function (renderPoint, i) {
                var point = points[i];
                renderPoint.set(point);
            });
        };

        PathAction.prototype.transform = function (translation, rotation, scale) {
            this.renderPoints.forEach(function (renderPoint) {
                renderPoint.multiply(scale);
                renderPoint.rotate(rotation);
                renderPoint.add(translation);
            });
        };

        PathAction.prototype.render = function (ctx) {
            this[this.method](ctx);
        };

        PathAction.prototype.move = function (ctx) {
            var point = this.renderPoints[0];
            ctx.moveTo(point.x, point.y);
        };

        PathAction.prototype.line = function (ctx) {
            var point = this.renderPoints[0];
            ctx.lineTo(point.x, point.y);
        };

        PathAction.prototype.bezier = function (ctx) {
            var cp0 = this.renderPoints[0];
            var cp1 = this.renderPoints[1];
            var end = this.renderPoints[2];
            ctx.bezierCurveTo(cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y);
        };

        PathAction.prototype.arc = function (ctx) {
            var prev = this.previousPoint;
            var corner = this.renderPoints[0];
            var end = this.renderPoints[1];
            var cp0 = this.controlPoints[0];
            var cp1 = this.controlPoints[1];
            cp0.set(prev).lerp(corner, 9 / 16);
            cp1.set(end).lerp(corner, 9 / 16);
            ctx.bezierCurveTo(cp0.x, cp0.y, cp1.x, cp1.y, end.x, end.y);
        };

        // -------------------------- Shape -------------------------- //

        function Shape(options) {
            this.create(options);
        }

        Shape.prototype.create = function (options) {
            // default
            extend(this, Shape.defaults);
            // set options
            setOptions(this, options);

            this.updatePathActions();

            // transform
            this.translate = new Vector3(options.translate);
            this.rotate = new Vector3(options.rotate);
            var scale = extend({
                x: 1,
                y: 1,
                z: 1
            }, options.scale);
            this.scale = new Vector3(scale);
            // children
            this.children = [];
            if (this.addTo) {
                this.addTo.addChild(this);
            }
        };

        Shape.defaults = {
            stroke: true,
            fill: false,
            color: 'black',
            lineWidth: 1,
            closed: true,
            rendering: true,
            path: [{}],
        };

        var optionKeys = Object.keys(Shape.defaults).concat([
            'rotate',
            'translate',
            'scale',
            'addTo',
            'width',
            'height',
        ]);

        function setOptions(shape, options) {
            for (var key in options) {
                if (optionKeys.includes(key)) {
                    shape[key] = options[key];
                }
            }
        }

        var actionNames = [
            'move',
            'line',
            'bezier',
            'arc',
        ];

        // parse path into PathActions
        Shape.prototype.updatePathActions = function () {
            var previousPoint;
            this.pathActions = this.path.map(function (pathPart, i) {
                // pathPart can be just vector coordinates -> { x, y, z }
                // or path instruction -> { arc: [ {x0,y0,z0}, {x1,y1,z1} ] }
                var keys = Object.keys(pathPart);
                var method = keys[0];
                var points = pathPart[method];
                var isInstruction = keys.length === 1 && actionNames.includes(method) &&
                    Array.isArray(points);

                if (!isInstruction) {
                    method = 'line';
                    points = [pathPart];
                }

                // first action is always move
                method = i === 0 ? 'move' : method;
                // arcs require previous last point
                var pathAction = new PathAction(method, points, previousPoint);
                // update previousLastPoint
                previousPoint = pathAction.endRenderPoint;
                return pathAction;
            });
        };

        Shape.prototype.addChild = function (shape) {
            this.children.push(shape);
        };

        // ----- update ----- //

        Shape.prototype.update = function () {
            // update self
            this.reset();
            // update children
            this.children.forEach(function (child) {
                child.update();
            });
            this.transform(this.translate, this.rotate, this.scale);
        };

        Shape.prototype.reset = function () {
            // reset pathAction render points
            this.pathActions.forEach(function (pathAction) {
                pathAction.reset();
            });
        };

        Shape.prototype.transform = function (translation, rotation, scale) {
            // transform points
            this.pathActions.forEach(function (pathAction) {
                pathAction.transform(translation, rotation, scale);
            });
            // transform children
            this.children.forEach(function (child) {
                child.transform(translation, rotation, scale);
            });
        };

        Shape.prototype.updateSortValue = function () {
            var sortValueTotal = 0;
            this.pathActions.forEach(function (pathAction) {
                sortValueTotal += pathAction.endRenderPoint.z;
            });
            // average sort value of all points
            // def not geometrically correct, but works for me
            this.sortValue = sortValueTotal / this.pathActions.length;
        };

        // ----- render ----- //

        Shape.prototype.render = function (ctx) {
            var length = this.pathActions.length;
            if (!this.rendering || !length) {
                return;
            }
            var isDot = length == 1;
            if (isDot) {
                this.renderDot(ctx);
            } else {
                this.renderPath(ctx);
            }
        };

        // Safari does not render lines with no size, have to render circle instead
        Shape.prototype.renderDot = function (ctx) {
            ctx.fillStyle = this.color;
            var point = this.pathActions[0].endRenderPoint;
            ctx.beginPath();
            var radius = this.lineWidth / 2;
            ctx.arc(point.x, point.y, radius, 0, TAU);
            ctx.fill();
        };

        Shape.prototype.renderPath = function (ctx) {
            // set render properties
            ctx.fillStyle = this.color;
            ctx.strokeStyle = this.color;
            ctx.lineWidth = this.lineWidth;

            // render points
            ctx.beginPath();
            this.pathActions.forEach(function (pathAction) {
                pathAction.render(ctx);
            });
            var isTwoPoints = this.pathActions.length == 2 &&
                this.pathActions[1].method == 'line';
            if (!isTwoPoints && this.closed) {
                ctx.closePath();
            }
            if (this.stroke) {
                ctx.stroke();
            }
            if (this.fill) {
                ctx.fill();
            }
        };

        // return Array of self & all child shapes
        Shape.prototype.getShapes = function () {
            var shapes = [this];
            this.children.forEach(function (child) {
                var childShapes = child.getShapes();
                shapes = shapes.concat(childShapes);
            });
            return shapes;
        };

        Shape.prototype.copy = function (options) {
            // copy options
            var shapeOptions = {};
            optionKeys.forEach(function (key) {
                shapeOptions[key] = this[key];
            }, this);
            // add set options
            setOptions(shapeOptions, options);
            var ShapeClass = this.constructor;
            return new ShapeClass(shapeOptions);
        };

        // -------------------------- Ellipse -------------------------- //

        function Ellipse(options) {
            options = this.setPath(options);
            // always keep open
            // fixes overlap bug when lineWidth is greater than radius
            options.closed = false;
            this.create(options);
        }

        Ellipse.prototype = Object.create(Shape.prototype);
        Ellipse.prototype.constructor = Ellipse;

        Ellipse.prototype.setPath = function (options) {
            var w = options.width / 2;
            var h = options.height / 2;
            options.path = [{
                    x: 0,
                    y: -h
                },
                {
                    arc: [ // top right
                        {
                            x: w,
                            y: -h
                        },
                        {
                            x: w,
                            y: 0
                        },
                    ]
                },
                {
                    arc: [ // bottom right
                        {
                            x: w,
                            y: h
                        },
                        {
                            x: 0,
                            y: h
                        },
                    ]
                },
                {
                    arc: [ // bottom left
                        {
                            x: -w,
                            y: h
                        },
                        {
                            x: -w,
                            y: 0
                        },
                    ]
                },
                {
                    arc: [ // bottom left
                        {
                            x: -w,
                            y: -h
                        },
                        {
                            x: 0,
                            y: -h
                        },
                    ]
                },
            ];
            return options;
        };

        // -------------------------- Group -------------------------- //

        function Group(options) {
            this.create(options);
        }

        Group.prototype.create = function (options) {
            // set options
            setGroupOptions(this, options);

            // transform
            this.translate = Vector3.sanitize(this.translate);
            this.rotate = Vector3.sanitize(this.rotate);
            // children
            this.children = [];
            if (this.addTo) {
                this.addTo.addChild(this);
            }
        };

        var groupOptionKeys = [
            'rotate',
            'translate',
            'addTo',
        ];

        function setGroupOptions(shape, options) {
            for (var key in options) {
                if (groupOptionKeys.includes(key)) {
                    shape[key] = options[key];
                }
            }
        }


        Group.prototype.addChild = function (shape) {
            this.children.push(shape);
        };

        // ----- update ----- //

        Group.prototype.update = function () {
            // update self
            this.reset();
            // update children
            this.children.forEach(function (child) {
                child.update();
            });
            this.transform(this.translate, this.rotate, this.scale);
        };

        Group.prototype.reset = function () {};

        Group.prototype.transform = function (translation, rotation, scale) {
            // transform children
            this.children.forEach(function (child) {
                child.transform(translation, rotation, scale);
            });
        };

        Group.prototype.updateSortValue = function () {
            var sortValueTotal = 0;
            this.children.forEach(function (child) {
                child.updateSortValue();
                sortValueTotal += child.sortValue;
            });
            // TODO sort children?
            // average sort value of all points
            // def not geometrically correct, but works for me
            this.sortValue = sortValueTotal / this.children.length;
        };

        // ----- render ----- //

        Group.prototype.render = function (ctx) {
            this.children.forEach(function (child) {
                child.render(ctx);
            });
        };

        // do not include children, group handles rendering & sorting internally
        Group.prototype.getShapes = function () {
            return [this];
        };

        // -------------------------- Dragger -------------------------- //

        // quick & dirty drag event stuff
        // messes up if multiple pointers/touches

        // event support, default to mouse events
        var downEvent = 'mousedown';
        var moveEvent = 'mousemove';
        var upEvent = 'mouseup';
        if (window.PointerEvent) {
            // PointerEvent, Chrome
            downEvent = 'pointerdown';
            moveEvent = 'pointermove';
            upEvent = 'pointerup';
        } else if ('ontouchstart' in window) {
            // Touch Events, iOS Safari
            downEvent = 'touchstart';
            moveEvent = 'touchmove';
            upEvent = 'touchend';
        }

        function noop() {}

        function Dragger(options) {
            this.startElement = options.startElement;
            this.onPointerDown = options.onPointerDown || noop;
            this.onPointerMove = options.onPointerMove || noop;
            this.onPointerUp = options.onPointerUp || noop;

            this.startElement.addEventListener(downEvent, this);
        }

        Dragger.prototype.handleEvent = function (event) {
            var method = this['on' + event.type];
            if (method) {
                method.call(this, event);
            }
        };

        Dragger.prototype.onmousedown =
            Dragger.prototype.onpointerdown = function (event) {
                this.pointerDown(event, event);
            };

        Dragger.prototype.ontouchstart = function (event) {
            this.pointerDown(event, event.changedTouches[0]);
        };

        Dragger.prototype.pointerDown = function (event, pointer) {
            event.preventDefault();
            this.dragStartX = pointer.pageX;
            this.dragStartY = pointer.pageY;
            window.addEventListener(moveEvent, this);
            window.addEventListener(upEvent, this);
            this.onPointerDown(pointer);
        };

        Dragger.prototype.ontouchmove = function (event) {
            // HACK, moved touch may not be first
            this.pointerMove(event, event.changedTouches[0]);
        };

        Dragger.prototype.onmousemove =
            Dragger.prototype.onpointermove = function (event) {
                this.pointerMove(event, event);
            };

        Dragger.prototype.pointerMove = function (event, pointer) {
            event.preventDefault();
            var moveX = pointer.pageX - this.dragStartX;
            var moveY = pointer.pageY - this.dragStartY;
            this.onPointerMove(pointer, moveX, moveY);
        };

        Dragger.prototype.onmouseup =
            Dragger.prototype.onpointerup =
            Dragger.prototype.ontouchend =
            Dragger.prototype.pointerUp = function (event) {
                window.removeEventListener(moveEvent, this);
                window.removeEventListener(upEvent, this);
                this.onPointerUp(event);
            };

        function BokehShape(options) {
            this.create(options);
            this.bokehSize = options.bokehSize || 5;
            this.bokehLimit = options.bokehLimit || 64;
        }

        BokehShape.prototype = Object.create(Shape.prototype);

        BokehShape.prototype.updateBokeh = function () {
            // bokeh 0 -> 1
            this.bokeh = Math.abs(this.sortValue) / this.bokehLimit;
            this.bokeh = Math.max(0, Math.min(1, this.bokeh));
            return this.bokeh;
        };

        BokehShape.prototype.getBokehLineWidth = function () {

            return this.lineWidth + this.bokehSize * this.bokeh * this.bokeh;
        };

        BokehShape.prototype.getBokehAlpha = function () {
            var revBokeh = 1 - this.bokeh;
            revBokeh *= revBokeh;
            return revBokeh * 0.8 + 0.2;
        };



        // Safari does not render lines with no size, have to render circle instead
        BokehShape.prototype.renderDot = function (ctx) {
            this.updateBokeh();
            ctx.globalAlpha = this.getBokehAlpha();
            ctx.fillStyle = this.color;
            var point = this.pathActions[0].endRenderPoint;
            ctx.beginPath();
            var radius = this.getBokehLineWidth() / 2;
            ctx.arc(point.x, point.y, radius, 0, TAU);
            ctx.fill();
            ctx.globalAlpha = 1;
        };

        BokehShape.prototype.renderPath = function (ctx) {
            this.updateBokeh();
            ctx.globalAlpha = this.getBokehAlpha();
            // set render properties
            ctx.fillStyle = this.color;
            ctx.strokeStyle = this.color;
            ctx.lineWidth = this.getBokehLineWidth();

            // render points
            ctx.beginPath();
            this.pathActions.forEach(function (pathAction) {
                pathAction.render(ctx);
            });
            var isTwoPoints = this.pathActions.length == 2 &&
                this.pathActions[1].method == 'line';
            if (!isTwoPoints && this.closed) {
                ctx.closePath();
            }
            if (this.stroke) {
                ctx.stroke();
            }
            if (this.fill) {
                ctx.fill();
            }
            ctx.globalAlpha = 1;
        };

        /* jshint unused: false */

        function makeMadeline(camera, isGood, colors, rotation) {

            var rotor = new Shape({
                rendering: false,
                addTo: camera,
                rotate: rotation,
            });

            var body = new Shape({
                rendering: false,
                addTo: rotor,
                rotate: {
                    x: TAU / 8
                },
                translate: {
                    z: 48
                },
            });

            var head = new Shape({
                rendering: false,
                addTo: body,
                translate: {
                    y: -11,
                    z: 2
                },
                rotate: {
                    x: -TAU / 8
                },
            });

            // face
            var face = new Ellipse({
                width: 6,
                height: 6,
                addTo: head,
                translate: {
                    z: -4
                },
                lineWidth: 8,
                color: colors.skin,
            });

            var eyeGroup = new Group({
                addTo: face,
                translate: {
                    z: -face.lineWidth / 2 + 0.5
                },
            });


            // eyes
            [-1, 1].forEach(function (xSide) {
                // cheek blush
                if (isGood) {
                    new Ellipse({
                        width: 2,
                        height: 1.3,
                        addTo: eyeGroup,
                        translate: {
                            x: 4.5 * xSide,
                            y: 3,
                            z: 1
                        },
                        rotate: {
                            y: TAU / 16 * xSide
                        },
                        lineWidth: 1,
                        color: '#FA8',
                        fill: true,
                    });
                }


                var eyeX = 3.5 * xSide;

                // eye
                var eyeWhite = new Ellipse({
                    width: 0.75,
                    height: 1.5,
                    addTo: eyeGroup,
                    color: colors.eye,
                    translate: {
                        x: eyeX
                    },
                    lineWidth: 2,
                    fill: true,
                });

                // eye brow
                new Shape({
                    path: [{
                            x: -1,
                            y: 0
                        },
                        {
                            arc: [{
                                    x: -1,
                                    y: -1
                                },
                                {
                                    x: 0,
                                    y: -1
                                }
                            ]
                        },
                        {
                            arc: [{
                                    x: 1,
                                    y: -1
                                },
                                {
                                    x: 1,
                                    y: 0
                                }
                            ]
                        },
                    ],
                    addTo: eyeGroup,
                    translate: {
                        x: eyeX,
                        y: -3
                    },
                    scale: {
                        x: 1.5,
                        y: 0.6
                    },
                    rotate: {
                        z: 0.15 * xSide * (isGood ? 1 : -1)
                    },
                    color: colors.hair,
                    lineWidth: 1,
                    fill: true,
                });



            });


            // hair ball
            new Shape({
                path: [{
                        x: -1
                    },
                    {
                        x: 1
                    },
                    {
                        z: 4
                    },
                ],
                addTo: head,
                translate: {
                    y: -4,
                    z: 1
                },
                lineWidth: 18,
                color: colors.hair,
            });

            var bang = new Shape({
                path: [{},
                    {
                        arc: [{
                                z: -4,
                                y: 4
                            },
                            {
                                z: 0,
                                y: 8
                            },
                        ]
                    },
                ],
                addTo: head,
                translate: {
                    x: 2,
                    y: -7.5,
                    z: -6
                },
                rotate: {
                    x: -0.5,
                    z: -0.5
                },
                lineWidth: 4,
                color: colors.hair,
                closed: false,
            });
            bang.copy({
                translate: {
                    x: 5,
                    y: -6,
                    z: -5
                },
                rotate: {
                    x: 0.3,
                    z: -0.5
                },
            });
            bang.copy({
                translate: {
                    x: 5,
                    y: -6,
                    z: -3
                },
                rotate: {
                    y: 0.7,
                    z: -1
                },
            });

            // left side
            bang.copy({
                translate: {
                    x: -2,
                    y: -7.5,
                    z: -6
                },
                rotate: {
                    x: 0,
                    z: TAU / 16 * 6
                },
            });
            bang.copy({
                translate: {
                    x: -5,
                    y: -6,
                    z: -5
                },
                rotate: {
                    x: 0,
                    z: TAU / 4
                },
            });
            bang.copy({
                translate: {
                    x: -5,
                    y: -6,
                    z: -3
                },
                rotate: {
                    y: -0.7,
                    z: 1
                },
            });

            // hair cover
            new Shape({
                path: [{
                        x: -3
                    },
                    {
                        x: 3
                    },
                ],
                addTo: head,
                lineWidth: 7,
                translate: {
                    y: -8,
                    z: -5
                },
                color: colors.hair,
            });

            // trail locks

            var trailLock = new Shape({
                path: [{
                        y: -4,
                        z: 0
                    },
                    {
                        bezier: [{
                                y: -10,
                                z: 14
                            },
                            {
                                y: 0,
                                z: 16
                            },
                            {
                                y: 0,
                                z: 26
                            }
                        ]
                    },
                ],
                addTo: head,
                translate: {
                    z: 4,
                    y: 0
                },
                lineWidth: 10,
                color: colors.hair,
                closed: false,
            });

            trailLock.copy({
                translate: {
                    x: -3,
                    z: 4
                },
                rotate: {
                    z: -TAU / 8
                },
                lineWidth: 8,
            });
            trailLock.copy({
                translate: {
                    x: 3,
                    z: 4
                },
                rotate: {
                    z: TAU / 8
                },
                lineWidth: 8,
            });
            trailLock.copy({
                translate: {
                    y: 2
                },
                // rotate: { z: TAU/2 },
                scale: {
                    y: 0.5
                },
                lineWidth: 8,
            });

            // ----- torso ----- //

            // 2nd rib
            var torsoRib = new Ellipse({
                width: 12,
                height: 10,
                addTo: body,
                rotate: {
                    x: TAU / 4
                },
                translate: {
                    y: -1
                },
                lineWidth: 6,
                color: colors.parkaLight,
                fill: true,
            });
            // neck rib
            torsoRib.copy({
                width: 6,
                height: 6,
                translate: {
                    y: -5
                },
            });
            // 3rd rib
            torsoRib.copy({
                translate: {
                    y: 3
                },
            });
            // 4th rib
            torsoRib.copy({
                translate: {
                    y: 7
                },
                color: colors.parkaDark,
            });
            // waist
            new Ellipse({
                width: 10,
                height: 8,
                addTo: body,
                rotate: {
                    x: TAU / 4
                },
                translate: {
                    y: 11
                },
                lineWidth: 4,
                color: colors.tight,
                fill: true,
            });

            // arms
            [-1, 1].forEach(function (xSide) {
                // shoulder ball
                new Shape({
                    addTo: body,
                    lineWidth: 6,
                    translate: {
                        x: 6 * xSide,
                        y: -5,
                        z: 1
                    },
                    color: colors.parkaLight,
                });

                var shoulderJoint = new Shape({
                    rendering: false,
                    addTo: body,
                    translate: {
                        x: 9 * xSide,
                        y: -3,
                        z: 2
                    },
                });

                // top shoulder rib
                var armRib = new Ellipse({
                    width: 2,
                    height: 2,
                    rotate: {
                        x: TAU / 4
                    },
                    addTo: shoulderJoint,
                    translate: {
                        x: 0 * xSide
                    },
                    lineWidth: 6,
                    color: colors.parkaLight,
                    fill: true,
                });
                armRib.copy({
                    translate: {
                        y: 4
                    },
                });

                var elbowJoint = new Shape({
                    rendering: false,
                    addTo: shoulderJoint,
                    translate: {
                        y: 8
                    },
                });

                armRib.copy({
                    addTo: elbowJoint,
                    translate: {
                        x: 0,
                        y: 0
                    },
                });
                armRib.copy({
                    addTo: elbowJoint,
                    translate: {
                        y: 4
                    },
                    color: colors.parkaDark,
                });

                // hand
                new Shape({
                    addTo: elbowJoint,
                    translate: {
                        y: 9,
                        z: -1
                    },
                    lineWidth: 8,
                    color: colors.skin,
                });

                if (xSide == 1) {
                    // extend left hand
                    shoulderJoint.rotate = Vector3.sanitize({
                        x: -TAU / 8 * 3,
                        z: -TAU / 32
                    });
                } else {
                    // back right hand
                    shoulderJoint.rotate = Vector3.sanitize({
                        z: TAU / 16 * 2,
                        x: TAU / 16 * 2
                    });
                    elbowJoint.rotate = Vector3.sanitize({
                        z: TAU / 8
                    });
                }

                // ----- legs ----- //
                var knee = {
                    y: 7
                };
                var thigh = new Shape({
                    path: [{
                        y: 0
                    }, knee],
                    addTo: body,
                    translate: {
                        x: 4 * xSide,
                        y: 13
                    },
                    lineWidth: 8,
                    color: colors.tight,
                });

                var shin = new Shape({
                    path: [{
                        y: 0
                    }, {
                        y: 8
                    }],
                    addTo: thigh,
                    lineWidth: 6,
                    translate: knee,
                    color: colors.tight,
                });

                if (xSide == -1) {
                    // bend right leg
                    thigh.rotate = Vector3.sanitize({
                        x: -TAU / 16 * 3,
                        z: TAU / 16
                    });
                    shin.rotate = Vector3.sanitize({
                        x: TAU / 16 * 5
                    });
                }

            });

            // butt
            new Shape({
                path: [{
                        x: -3
                    },
                    {
                        x: 3
                    },
                ],
                rendering: false,
                addTo: body,
                translate: {
                    y: 11,
                    z: 2
                },
                lineWidth: 8,
                color: colors.tight,
            });

        }


        function makeBird(options) {

            var spin = options.spin || 0;

            var arrow = new Shape({
                rendering: false,
                addTo: options.addTo,
                scale: {
                    x: 2 / 3,
                    y: 2 / 3,
                    z: 2 / 3
                },
                rotate: {
                    z: spin
                },
            });

            var bird = new Group({
                addTo: arrow,
                translate: {
                    x: 87
                },
                rotate: {
                    x: spin
                },
            });

            // bird body
            new Shape({
                path: [{
                        x: -3,
                        y: 0
                    },
                    {
                        arc: [{
                                x: -2,
                                y: 1.5
                            },
                            {
                                x: 0,
                                y: 1.5
                            },
                        ]
                    },
                    {
                        arc: [{
                                x: 2,
                                y: 1.5
                            },
                            {
                                x: 2,
                                y: 0
                            },
                        ]
                    },
                ],
                addTo: bird,
                translate: {
                    x: 0.5
                },
                lineWidth: 3,
                color: options.color,
                fill: true,
            });

            // bird head
            new Shape({
                translate: {
                    x: 4,
                    y: -1
                },
                addTo: bird,
                lineWidth: 4,
                color: options.color,
            });

            // beak
            new Shape({
                path: [{
                        x: 0,
                        y: -1
                    },
                    {
                        x: 3,
                        y: 0
                    },
                    {
                        x: 0,
                        y: 1
                    },
                ],
                addTo: bird,
                translate: {
                    x: 5,
                    y: -1
                },
                lineWidth: 1,
                color: options.color,
                fill: true,
            });

            // tail feather
            new Shape({
                path: [{
                        x: -3,
                        z: -2
                    },
                    {
                        x: 0,
                        z: 0
                    },
                    {
                        x: -3,
                        z: 2
                    },
                ],
                addTo: bird,
                translate: {
                    x: -4,
                    y: 0
                },
                lineWidth: 2,
                color: options.color,
                fill: true,
            });

            var wing = new Shape({
                path: [{
                        x: 3,
                        y: 0
                    },
                    {
                        x: -1,
                        y: -9
                    },
                    {
                        arc: [{
                                x: -5,
                                y: -4
                            },
                            {
                                x: -3,
                                y: 0
                            },
                        ]
                    },
                ],
                addTo: bird,
                translate: {
                    z: -1.5
                },
                rotate: {
                    x: TAU / 8
                },
                lineWidth: 1,
                color: options.color,
                fill: true,
            });

            wing.copy({
                translate: {
                    z: 1.5
                },
                scale: {
                    z: -1
                },
                rotate: {
                    x: -TAU / 8
                },
            });

        }

        /* globals makeMadeline, BokehShape, makeBird */

        // -------------------------- demo -------------------------- //

        var canvas = document.querySelector('canvas');
        var ctx = canvas.getContext('2d');
        var w = 160;
        var h = 160;
        var minWindowSize = Math.min(window.innerWidth, window.innerHeight);
        var zoom = Math.min(5, Math.floor(minWindowSize / w));
        var pixelRatio = window.devicePixelRatio || 1;
        zoom *= pixelRatio;
        var canvasWidth = canvas.width = w * zoom;
        var canvasHeight = canvas.height = h * zoom;
        // set canvas screen size
        if (pixelRatio > 1) {
            canvas.style.width = canvasWidth / pixelRatio + 'px';
            canvas.style.height = canvasHeight / pixelRatio + 'px';
        }

        var isRotating = true;

        var madColor = {
            skin: '#FD9',
            hair: '#D53',
            parkaLight: '#67F',
            parkaDark: '#35D',
            tight: '#742',
            eye: '#333',
        };
        var badColor = {
            skin: '#EBC',
            hair: '#D4B',
            parkaLight: '#85A',
            parkaDark: '#527',
            tight: '#412',
            eye: '#D02',
        };

        var glow = 'hsla(60, 100%, 80%, 0.3)';
        var featherGold = '#FE5';

        var camera = new Shape({
            rendering: false,
            rotate: {
                y: TAU / 4
            },
        });

        // -- illustration shapes --- //

        makeMadeline(camera, true, madColor);
        makeMadeline(camera, false, badColor, {
            y: TAU / 2
        });


        // ----- feather ----- //

        var feather = new Shape({
            rendering: false,
            addTo: camera,
            rotate: {
                y: -TAU / 4
            },
        });

        (function () {

            var featherPartCount = 8;
            var radius = 12;
            var angleX = (TAU / featherPartCount) / 2;
            var sector = (TAU * radius) / 2 / featherPartCount;

            for (var i = 0; i < featherPartCount; i++) {
                var curve = Math.cos((i / featherPartCount) * TAU * 3 / 4 + TAU * 1 / 4);
                var x = 4 - curve * 2;
                var y0 = sector / 2;
                // var y2 = -sector/2;
                var isLast = i == featherPartCount - 1;
                var y3 = isLast ? sector * -1 : -y0;
                var z1 = -radius + 2 + curve * -1.5;
                var z2 = isLast ? -radius : -radius;
                var barb = new Shape({
                    path: [{
                            x: 0,
                            y: y0,
                            z: -radius
                        },
                        {
                            x: x,
                            y: -sector / 2,
                            z: z1
                        },
                        {
                            x: x,
                            y: -sector * 3 / 4,
                            z: z1
                        },
                        {
                            x: 0,
                            y: y3,
                            z: z2
                        },
                    ],
                    addTo: feather,
                    rotate: {
                        x: angleX * -i + TAU / 8
                    },
                    lineWidth: 1,
                    color: featherGold,
                    fill: true,
                });
                barb.copy({
                    scale: {
                        x: -1
                    },
                });
            }

            // rachis
            var rachis = new Shape({
                path: [{
                        y: -radius
                    },
                    {
                        arc: [{
                                y: -radius,
                                z: -radius
                            },
                            {
                                y: 0,
                                z: -radius
                            },
                        ]
                    },
                    {
                        arc: [{
                                y: radius,
                                z: -radius
                            },
                            {
                                y: radius,
                                z: 0
                            },
                        ]
                    },
                ],
                addTo: feather,
                lineWidth: 2,
                color: featherGold,
                closed: false,
            });
            rachis.copy({
                lineWidth: 8,
                color: glow,
                rotate: {
                    x: -0.5
                }
            });
        })();

        // ----- rods ----- //

        (function () {

            var rodCount = 14;
            for (var i = 0; i < rodCount; i++) {
                var zRotor = new Shape({
                    rendering: false,
                    addTo: camera,
                    rotate: {
                        z: TAU / rodCount * i
                    },
                });

                var y0 = 32;
                var y1 = y0 + 2 + Math.random() * 24;
                new BokehShape({
                    path: [{
                            y: y0
                        },
                        {
                            y: y1
                        },
                    ],
                    addTo: zRotor,
                    rotate: {
                        x: (Math.random() * 2 - 1) * TAU / 8
                    },
                    color: madColor.skin,
                    lineWidth: 1,
                    bokehSize: 6,
                    bokehLimit: 70,
                });
            }

        })();

        // dots

        (function () {
            var dotCount = 64;

            for (var i = 0; i < dotCount; i++) {
                var yRotor = new Shape({
                    rendering: false,
                    addTo: camera,
                    rotate: {
                        y: TAU / dotCount * i
                    },
                });

                new BokehShape({
                    path: [{
                        z: 40 * (1 - Math.random() * Math.random()) + 32
                    }, ],
                    addTo: yRotor,
                    rotate: {
                        x: (Math.random() * 2 - 1) * TAU * 3 / 16
                    },
                    color: badColor.skin,
                    lineWidth: 1 + Math.random(),
                    bokehSize: 6,
                    bokehLimit: 74,
                });
            }

        })();

        // ----- birds ----- //

        var birdRotor = new Shape({
            rendering: false,
            addTo: camera,
            rotate: {
                y: TAU * 1 / 8
            },
        });

        makeBird({
            addTo: birdRotor,
            color: madColor.parkaLight,
            spin: TAU / 2,
        });

        makeBird({
            addTo: birdRotor,
            color: featherGold,
            spin: -TAU * 3 / 8,
        });

        makeBird({
            addTo: birdRotor,
            color: 'white',
            spin: -TAU / 4,
        });

        makeBird({
            addTo: birdRotor,
            color: madColor.hair,
            spin: -TAU / 8,
        });

        makeBird({
            addTo: birdRotor,
            color: madColor.parkaDark,
            spin: TAU / 8,
        });

        // -----  ----- //

        var shapes = camera.getShapes();

        // -- animate --- //

        var rotateSpeed = -TAU / 60;
        var xClock = 0;
        var then = new Date() - 1 / 60;

        function animate() {
            update();
            render();
            requestAnimationFrame(animate);
        }

        animate();

        // -- update -- //

        function update() {
            var now = new Date();
            var delta = now - then;
            // auto rotate
            if (isRotating) {
                var theta = rotateSpeed / 60 * delta;
                camera.rotate.y += theta;
                xClock += theta / 4;
                camera.rotate.x = Math.sin(xClock) * TAU / 12;
            }

            // rotate
            camera.update();
            shapes.forEach(function (shape) {
                shape.updateSortValue();
            });
            // perspective sort
            shapes.sort(function (a, b) {
                return b.sortValue - a.sortValue;
            });

            then = now;
        }

        // -- render -- //

        function render() {
            ctx.clearRect(0, 0, canvasWidth, canvasHeight);
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';

            ctx.save();
            ctx.scale(zoom, zoom);
            ctx.translate(w / 2, h / 2);

            shapes.forEach(function (shape) {
                shape.render(ctx);
            });

            ctx.restore();
        }

        // ----- inputs ----- //

        // click drag to rotate
        var dragStartAngleX, dragStartAngleY;

        new Dragger({
            startElement: canvas,
            onPointerDown: function () {
                isRotating = false;
                dragStartAngleX = camera.rotate.x;
                dragStartAngleY = camera.rotate.y;
            },
            onPointerMove: function (pointer, moveX, moveY) {
                var angleXMove = moveY / canvasWidth * TAU;
                var angleYMove = moveX / canvasWidth * TAU;
                camera.rotate.x = dragStartAngleX + angleXMove;
                camera.rotate.y = dragStartAngleY + angleYMove;
            },
        });
    </script>
</body>

</html>